package com.fr.drschizzo.raycaster.core;

import static playn.core.PlayN.graphics;
import static playn.core.PlayN.keyboard;
import playn.core.Canvas;
import playn.core.CanvasImage;
import playn.core.Game;
import playn.core.ImageLayer;
import playn.core.Key;
import playn.core.Keyboard;
import playn.core.Keyboard.Event;

public class RaycasterExemple implements Game {

	// size of tile (wall height)
	static final int TILE_SIZE = 64;
	static final int WALL_HEIGHT = 64;
	static final int PROJECTIONPLANEWIDTH = 320;
	static final int PROJECTIONPLANEHEIGHT = 200;
	static final int ANGLE60 = PROJECTIONPLANEWIDTH;
	static final int ANGLE30 = (ANGLE60 / 2);
	static final int ANGLE15 = (ANGLE30 / 2);
	static final int ANGLE90 = (ANGLE30 * 3);
	static final int ANGLE180 = (ANGLE90 * 2);
	static final int ANGLE270 = (ANGLE90 * 3);
	static final int ANGLE360 = (ANGLE60 * 6);
	static final int ANGLE0 = 0;
	static final int ANGLE5 = (ANGLE30 / 6);
	static final int ANGLE10 = (ANGLE5 * 2);

	// trigonometric tables
	float fSinTable[];
	float fISinTable[];
	float fCosTable[];
	float fICosTable[];
	float fTanTable[];
	float fITanTable[];
	float fFishTable[];
	float fXStepTable[];
	float fYStepTable[];

	// player's attributes
	int fPlayerX = 100;
	int fPlayerY = 160;
	int fPlayerArc = ANGLE0;
	int fPlayerDistanceToTheProjectionPlane = 277;
	int fPlayerHeight = 32;
	int fPlayerSpeed = 16;
	int fProjectionPlaneYCenter = PROJECTIONPLANEHEIGHT / 2;
	// the following variables are used to keep the player coordinate in the
	// overhead map
	int fPlayerMapX, fPlayerMapY, fMinimapWidth;

	// movement flag
	boolean fKeyUp = false;
	boolean fKeyDown = false;
	boolean fKeyLeft = false;
	boolean fKeyRight = false;

	// 2 dimensional map
	byte fMap[];
	static final byte W = 1; // wall
	static final byte O = 0; // opening
	static final int MAP_WIDTH = 12;
	static final int MAP_HEIGHT = 12;

	// *******************************************************************//
	// * Convert arc to radian
	// *******************************************************************//
	float arcToRad(float arcAngle) {
		return ((float) (arcAngle * Math.PI) / ANGLE180);
	}

	// *******************************************************************//
	// * Create tigonometric values to make the program runs faster.
	// *******************************************************************//
	public void createTables() {
		int i;
		float radian;
		fSinTable = new float[ANGLE360 + 1];
		fISinTable = new float[ANGLE360 + 1];
		fCosTable = new float[ANGLE360 + 1];
		fICosTable = new float[ANGLE360 + 1];
		fTanTable = new float[ANGLE360 + 1];
		fITanTable = new float[ANGLE360 + 1];
		fFishTable = new float[ANGLE60 + 1];
		fXStepTable = new float[ANGLE360 + 1];
		fYStepTable = new float[ANGLE360 + 1];

		for (i = 0; i <= ANGLE360; i++) {
			// get the radian value (the last addition is to avoid division by
			// 0, try removing
			// that and you'll see a hole in the wall when a ray is at 0, 90,
			// 180, or 270 degree)
			radian = arcToRad(i) + (float) (0.0001);
			fSinTable[i] = (float) Math.sin(radian);
			fISinTable[i] = (1.0F / (fSinTable[i]));
			fCosTable[i] = (float) Math.cos(radian);
			fICosTable[i] = (1.0F / (fCosTable[i]));
			fTanTable[i] = (float) Math.tan(radian);
			fITanTable[i] = (1.0F / fTanTable[i]);

			// you can see that the distance between xi is the same
			// if we know the angle
			// _____|_/next xi______________
			// |
			// ____/|next xi_________ slope = tan = height / dist between xi's
			// / |
			// __/__|_________ dist between xi = height/tan where height=tile
			// size
			// old xi|
			// distance between xi = x_step[view_angle];
			//
			//
			// facine left
			// facing left
			if (i >= ANGLE90 && i < ANGLE270) {
				fXStepTable[i] = (TILE_SIZE / fTanTable[i]);
				if (fXStepTable[i] > 0)
					fXStepTable[i] = -fXStepTable[i];
			}
			// facing right
			else {
				fXStepTable[i] = (TILE_SIZE / fTanTable[i]);
				if (fXStepTable[i] < 0)
					fXStepTable[i] = -fXStepTable[i];
			}

			// FACING DOWN
			if (i >= ANGLE0 && i < ANGLE180) {
				fYStepTable[i] = (TILE_SIZE * fTanTable[i]);
				if (fYStepTable[i] < 0)
					fYStepTable[i] = -fYStepTable[i];
			}
			// FACING UP
			else {
				fYStepTable[i] = (TILE_SIZE * fTanTable[i]);
				if (fYStepTable[i] > 0)
					fYStepTable[i] = -fYStepTable[i];
			}
		}

		for (i = -ANGLE30; i <= ANGLE30; i++) {
			radian = arcToRad(i);
			// we don't have negative angle, so make it start at 0
			// this will give range 0 to 320
			fFishTable[i + ANGLE30] = (float) (1.0F / Math.cos(radian));
		}

		// CERATE A SIMPLE MAP
		byte[] map = { W, W, W, W, W, W, W, W, W, W, W, W, W, O, O, O, O, O, O,
				O, O, O, O, W, W, O, O, O, O, O, O, O, O, O, O, W, W, O, O, O,
				O, O, O, O, W, O, O, W, W, O, O, W, O, W, O, O, W, O, O, W, W,
				O, O, W, O, W, W, O, W, O, O, W, W, O, O, W, O, O, W, O, W, O,
				O, W, W, O, O, O, W, O, W, O, W, O, O, W, W, O, O, O, W, O, W,
				O, W, O, O, W, W, O, O, O, W, W, W, O, W, O, O, W, W, O, O, O,
				O, O, O, O, O, O, O, W, W, W, W, W, W, W, W, W, W, W, W, W };
		fMap = map;
	}

	// *******************************************************************//
	// * Renderer
	// *******************************************************************//
	public void render() {

		int verticalGrid; // horizotal or vertical coordinate of intersection
		int horizontalGrid; // theoritically, this will be multiple of TILE_SIZE
							// , but some trick did here might cause
							// the values off by 1
		int distToNextVerticalGrid; // how far to the next bound (this is
									// multiple of
		int distToNextHorizontalGrid; // tile size)
		float xIntersection; // x and y intersections
		float yIntersection;
		float distToNextXIntersection;
		float distToNextYIntersection;

		int xGridIndex; // the current cell that the ray is in
		int yGridIndex;

		float distToVerticalGridBeingHit; // the distance of the x and y ray
											// intersections from
		float distToHorizontalGridBeingHit; // the viewpoint

		int castArc, castColumn;

		castArc = fPlayerArc;
		// field of view is 60 degree with the point of view (player's direction
		// in the middle)
		// 30 30
		// ^
		// \ | /
		// \|/
		// v
		// we will trace the rays starting from the leftmost ray
		castArc -= ANGLE30;
		// wrap around if necessary
		if (castArc < 0) {
			castArc = ANGLE360 + castArc;
		}

		for (castColumn = 0; castColumn < PROJECTIONPLANEWIDTH; castColumn += 1) {
			// ray is between 0 to 180 degree (1st and 2nd quadrant)
			// ray is facing down
			if (castArc > ANGLE0 && castArc < ANGLE180) {
				// truncuate then add to get the coordinate of the FIRST grid
				// (horizontal
				// wall) that is in front of the player (this is in pixel unit)
				// ROUND DOWN
				horizontalGrid = (fPlayerY / TILE_SIZE) * TILE_SIZE + TILE_SIZE;

				// compute distance to the next horizontal wall
				distToNextHorizontalGrid = TILE_SIZE;

				float xtemp = fITanTable[castArc] * (horizontalGrid - fPlayerY);
				// we can get the vertical distance to that wall by
				// (horizontalGrid-GLplayerY)
				// we can get the horizontal distance to that wall by
				// 1/tan(arc)*verticalDistance
				// find the x interception to that wall
				xIntersection = xtemp + fPlayerX;
			}
			// else, the ray is facing up
			else {
				horizontalGrid = (fPlayerY / TILE_SIZE) * TILE_SIZE;
				distToNextHorizontalGrid = -TILE_SIZE;

				float xtemp = fITanTable[castArc] * (horizontalGrid - fPlayerY);
				xIntersection = xtemp + fPlayerX;

				horizontalGrid--;
			}
			// LOOK FOR HORIZONTAL WALL
			if (castArc == ANGLE0 || castArc == ANGLE180) {
				distToHorizontalGridBeingHit = 9999999F;// Float.MAX_VALUE;
			}
			// else, move the ray until it hits a horizontal wall
			else {
				distToNextXIntersection = fXStepTable[castArc];
				while (true) {
					xGridIndex = (int) (xIntersection / TILE_SIZE);
					// in the picture, yGridIndex will be 1
					yGridIndex = (horizontalGrid / TILE_SIZE);

					if ((xGridIndex >= MAP_WIDTH) || (yGridIndex >= MAP_HEIGHT)
							|| xGridIndex < 0 || yGridIndex < 0) {
						distToHorizontalGridBeingHit = Float.MAX_VALUE;
						break;
					} else if ((fMap[yGridIndex * MAP_WIDTH + xGridIndex]) != O) {
						distToHorizontalGridBeingHit = (xIntersection - fPlayerX)
								* fICosTable[castArc];
						break;
					}
					// else, the ray is not blocked, extend to the next block
					else {
						xIntersection += distToNextXIntersection;
						horizontalGrid += distToNextHorizontalGrid;
					}
				}
			}

			// FOLLOW X RAY
			if (castArc < ANGLE90 || castArc > ANGLE270) {
				verticalGrid = TILE_SIZE + (fPlayerX / TILE_SIZE) * TILE_SIZE;
				distToNextVerticalGrid = TILE_SIZE;

				float ytemp = fTanTable[castArc] * (verticalGrid - fPlayerX);
				yIntersection = ytemp + fPlayerY;
			}
			// RAY FACING LEFT
			else {
				verticalGrid = (fPlayerX / TILE_SIZE) * TILE_SIZE;
				distToNextVerticalGrid = -TILE_SIZE;

				float ytemp = fTanTable[castArc] * (verticalGrid - fPlayerX);
				yIntersection = ytemp + fPlayerY;

				verticalGrid--;
			}
			// LOOK FOR VERTICAL WALL
			if (castArc == ANGLE90 || castArc == ANGLE270) {
				distToVerticalGridBeingHit = 9999999;// Float.MAX_VALUE;
			} else {
				distToNextYIntersection = fYStepTable[castArc];
				while (true) {
					// compute current map position to inspect
					xGridIndex = (verticalGrid / TILE_SIZE);
					yGridIndex = (int) (yIntersection / TILE_SIZE);

					if ((xGridIndex >= MAP_WIDTH) || (yGridIndex >= MAP_HEIGHT)
							|| xGridIndex < 0 || yGridIndex < 0) {
						distToVerticalGridBeingHit = Float.MAX_VALUE;
						break;
					} else if ((fMap[yGridIndex * MAP_WIDTH + xGridIndex]) != O) {
						distToVerticalGridBeingHit = (yIntersection - fPlayerY)
								* fISinTable[castArc];
						break;
					} else {
						yIntersection += distToNextYIntersection;
						verticalGrid += distToNextVerticalGrid;
					}
				}
			}

			// DRAW THE WALL SLICE
			float scaleFactor;
			float dist;
			int topOfWall; // used to compute the top and bottom of the sliver
							// that
			int bottomOfWall; // will be the staring point of floor and ceiling
			// determine which ray strikes a closer wall.
			// if yray distance to the wall is closer, the yDistance will be
			// shorter than
			// the xDistance
			if (distToHorizontalGridBeingHit < distToVerticalGridBeingHit) {
				// the next function call (drawRayOnMap()) is not a part of
				// raycating rendering part,
				// it just draws the ray on the overhead map to illustrate the
				// raycasting process
				dist = distToHorizontalGridBeingHit;
				canvas.setStrokeColor(0xFF0000FF);
			}
			// else, we use xray instead (meaning the vertical wall is closer
			// than
			// the horizontal wall)
			else {
				// the next function call (drawRayOnMap()) is not a part of
				// raycating rendering part,
				// it just draws the ray on the overhead map to illustrate the
				// raycasting process
				dist = distToVerticalGridBeingHit;
				canvas.setStrokeColor(0xAA0000FF);
			}

			// correct distance (compensate for the fishbown effect)
			dist /= fFishTable[castColumn];
			// projected_wall_height/wall_height =
			// fPlayerDistToProjectionPlane/dist;
			int projectedWallHeight = (int) (WALL_HEIGHT
					* (float) fPlayerDistanceToTheProjectionPlane / dist);
			bottomOfWall = fProjectionPlaneYCenter
					+ (int) (projectedWallHeight * 0.5F);
			topOfWall = PROJECTIONPLANEHEIGHT - bottomOfWall;
			if (bottomOfWall >= PROJECTIONPLANEHEIGHT)
				bottomOfWall = PROJECTIONPLANEHEIGHT - 1;
			canvas.drawLine(castColumn, topOfWall, castColumn, bottomOfWall);
			// canvas.(castColumn, topOfWall, 5, projectedWallHeight);

			// TRACE THE NEXT RAY
			castArc += 1;
			if (castArc >= ANGLE360)
				castArc -= ANGLE360;
		}

		// blit to screen

	}

	CanvasImage canvasImage = graphics().createImage(320, 200);
	Canvas canvas = canvasImage.canvas();
	ImageLayer canvasLayer = graphics().createImageLayer();

	float wallHeight = 64;
	float tileSize = 64;

	boolean moveleft = false;
	boolean moveright = false;
	boolean moveup = false;
	boolean movedown = false;

	byte[][] map = { { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
			{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
			{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
			{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1 },
			{ 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1 },
			{ 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1 },
			{ 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1 },
			{ 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1 },
			{ 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1 },
			{ 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1 },
			{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
			{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 } };

	@Override
	public void init() {
		graphics().setSize(320, 200);
		canvasLayer.setImage(canvasImage);
		graphics().rootLayer().add(canvasLayer);

		createTables();

		keyboard().setListener(new Keyboard.Adapter() {
			@Override
			public void onKeyDown(Event event) {
				if (event.key().equals(Key.LEFT)) {
					moveleft = true;
				}
				if (event.key().equals(Key.RIGHT)) {
					moveright = true;
				}
				if (event.key().equals(Key.UP)) {
					moveup = true;
				}
				if (event.key().equals(Key.DOWN)) {
					movedown = true;
				}

			}

			@Override
			public void onKeyUp(Event event) {
				if (event.key().equals(Key.LEFT)) {
					moveleft = false;
				}
				if (event.key().equals(Key.RIGHT)) {
					moveright = false;
				}
				if (event.key().equals(Key.UP)) {
					moveup = false;
				}
				if (event.key().equals(Key.DOWN)) {
					movedown = false;
				}

			}

		});
	}

	@Override
	public void paint(float alpha) {
		// the background automatically paints itself, so no need to do anything
		// here!
	}

	boolean facingUp(float angle) {
		if (angle >= 0 && angle < 180) {
			return true;
		}
		return false;
	}

	boolean facingRight(float angle) {
		if (angle < 90 || angle > 270) {
			return true;
		}
		return false;
	}

	private float computeAngle(float angle) {
		if (angle > 360)
			angle = angle % 360;
		if (angle < 0)
			angle = 360 + angle;
		return angle;
	}

	@Override
	public void update(float delta) {

		canvas.clear();
		if (moveleft) {
			if ((fPlayerArc -= ANGLE10) < ANGLE0)
				fPlayerArc += ANGLE360;
		}
		// rotate right
		else if (moveright) {
			if ((fPlayerArc += ANGLE10) >= ANGLE360)
				fPlayerArc -= ANGLE360;
		}

		// _____ _
		// |\ arc |
		// | \ y
		// | \ |
		// -
		// |--x--|
		//
		// sin(arc)=y/diagonal
		// cos(arc)=x/diagonal where diagonal=speed
		float playerXDir = fCosTable[fPlayerArc];
		float playerYDir = fSinTable[fPlayerArc];

		// move forward
		if (moveup) {
			fPlayerX += (int) (playerXDir * fPlayerSpeed);
			fPlayerY += (int) (playerYDir * fPlayerSpeed);
		}
		// move backward
		else if (movedown) {
			fPlayerX -= (int) (playerXDir * fPlayerSpeed);
			fPlayerY -= (int) (playerYDir * fPlayerSpeed);
		}

		render();

	}

	@Override
	public int updateRate() {
		return 25;
	}
}
